# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: chensili

import logging

from coin.base.timestamp import get_timestamp
from coin.exchange.base.order_gateway import OrderSide
from coin.proto.coin_executor_pb2 import PassiveExecutorConfig
from coin.exchange.util.rate_limit import RateLimiter
from coin.exchange.util.qty_limiter import QtyLimiter
from coin.strategy.executor.passive.interface import PassiveExecutorInterface
from coin.strategy.executor.util.cancel_executor import CancelExecutor
from coin.exchange.util.cooldown_limit import CoolDownLimiter
from coin.strategy.executor.util.last_fill_timestamp import LastFillTimestamp
from coin.strategy.executor.util.last_fire_timestamp import LastFireTimestamp
from coin.strategy.executor.util.order_adjuster import ProductInfoAdjuster
from coin.strategy.executor.util.config_adjuster import ExecutorConfigAdjuster
from coin.strategy.executor.util.working_order_state import (WorkingOrderStateGenerator)
from coin.support.telemetry.latency_recorder import LifeOfSignal, use_latency_recorder


def _in_price(x, y, price):
  return abs(x - y) <= price


def _in_bp(x, y, bp):
  if x > 0 and y > 0:
    return (abs(x - y) / (x + y) * 2) <= (bp * 1e-4)
  else:
    return False


class PassiveExecutor(PassiveExecutorInterface):
  ProductType = None
  OGType = None
  Traits = None

  def __init__(self, config, order_gateway, *, logger=None, latency_recorder=None):
    assert isinstance(order_gateway, self.OGType), (order_gateway, self.OGType)
    assert type(config) == PassiveExecutorConfig, type(config)
    self._logger = logger or logging.getLogger('PassiveExecutor')
    self._config2 = ExecutorConfigAdjuster(config)
    self._product = self.ProductType.FromStr(self.config.symbol)
    self._product_adjuster = ProductInfoAdjuster(self._product)
    self._og = order_gateway
    self._traits = self.Traits(self._config2, order_gateway, self._product)

    self._logger.info('product: %s', self._product)
    self._logger.info("executor config:\n%s", self.config)
    self._latency_recorder = latency_recorder
    self._og.set_latency_recorder(self._latency_recorder)

    self._position_timestamp = None
    self._position = None
    self._wo = WorkingOrderStateGenerator(product=self._product,
                                          og=self._og,
                                          sell_order_side=self._traits.SellOrderSide,
                                          buy_order_side=self._traits.BuyOrderSide)
    cooldown_limiter = CoolDownLimiter(0.5e9)
    self._cancel_executor = CancelExecutor(self._wo, cooldown_limiter)
    self._sell_rate_limiter = RateLimiter(self.config.rate_limiter_rate,
                                          self.config.rate_limiter_per)
    self._buy_rate_limiter = RateLimiter(self.config.rate_limiter_rate,
                                         self.config.rate_limiter_per)
    if self.config.HasField('fill_limiter_qty'):
      self._sell_fill_limiter_qty = QtyLimiter(self.config.fill_limiter_qty,
                                               self.config.fill_limiter_per)
      self._buy_fill_limiter_qty = QtyLimiter(self.config.fill_limiter_qty,
                                              self.config.fill_limiter_per)
    else:
      self._sell_fill_limiter_qty = None
      self._buy_fill_limiter_qty = None
    self._lfiret = LastFireTimestamp(self.config)
    self._lfillt = LastFillTimestamp(self.config)
    self._reset_action()

  @property
  def config(self):
    # config = constant config
    # config2 = adjustable config
    return self._config2.config

  @property
  def config2(self):
    # config = constant config
    # config2 = adjustable config
    return self._config2

  @property
  def product(self):
    return self._product

  @property
  def og(self):
    return self._og

  @property
  def logger(self):
    return self._logger

  def update_tick(self):  # TODO(xguo): Remove later
    self._product_adjuster = ProductInfoAdjuster(self._product)

  def query_unbiased_position(self):
    return self._traits._get_position(self._product)

  def set_config(self, config):
    assert type(config) == PassiveExecutorConfig, type(config)
    self._config2.set_config(config)

  def set_close_only(self, mode):
    self._config2.set_close_only(mode)

  def _reset_action(self):
    self._cancel_executor.clear()
    self._fire_sell = False
    self._fire_buy = False

  def _update_state(self, timestamp):
    prev_position = self._position
    position = self._traits._get_position(self._product)
    pos_change = (type(prev_position) != type(position) or prev_position != position)
    if pos_change:
      # TODO(jshin): not yet possible to differentiate buy/sell fill. set both.
      self._lfillt.set_last_sell_fill(timestamp)
      self._lfillt.set_last_buy_fill(timestamp)
    if pos_change and self._sell_fill_limiter_qty and prev_position is not None:
      delta_position = position - prev_position
      self._sell_fill_limiter_qty.check(timestamp, -delta_position)
      self._buy_fill_limiter_qty.check(timestamp, delta_position)

    self._position_timestamp = timestamp
    self._position = position
    self._wo._update_order_state()

  def _adjust_request(self, sell_price, buy_price, sell_qty, buy_qty):
    # adjust default size
    if sell_price is not None:
      sell_qty = sell_qty or self.config2.lot_size
      assert sell_qty > 0
    if buy_price is not None:
      buy_qty = buy_qty or self.config2.lot_size
      assert buy_qty > 0

    sell_qty, buy_qty = self._adjust_qty(sell_qty, buy_qty)

    # adjust min_lot_size_ratio, max_lot_size_ratio
    if (sell_qty is not None and sell_qty < self.config2.lot_size * self.config.min_lot_size_ratio):
      sell_qty = None
    if (sell_qty is not None and sell_qty > self.config2.lot_size * self.config.max_lot_size_ratio):
      sell_qty = self.config2.lot_size * self.config.max_lot_size_ratio
    if (buy_qty is not None and buy_qty < self.config2.lot_size * self.config.min_lot_size_ratio):
      buy_qty = None
    if (buy_qty is not None and buy_qty > self.config2.lot_size * self.config.max_lot_size_ratio):
      buy_qty = self.config2.lot_size * self.config.max_lot_size_ratio

    # adjust product
    if sell_price is not None:
      sell_price = self._product_adjuster.ceil_price(sell_price)
      sell_qty = self._product_adjuster.floor_qty(sell_qty)
    if buy_price is not None:
      buy_price = self._product_adjuster.floor_price(buy_price)
      buy_qty = self._product_adjuster.floor_qty(buy_qty)

    return sell_price, buy_price, sell_qty, buy_qty

  def _adjust_qty(self, sell_qty, buy_qty):
    # adjust default size
    if sell_qty is not None:
      sell_qty = sell_qty or self.config2.lot_size
      assert sell_qty > 0
    if buy_qty is not None:
      buy_qty = buy_qty or self.config2.lot_size
      assert buy_qty > 0

    # adjust log2_buy_sell_ratio
    if self.config.log2_buy_sell_ratio > 0:
      # keep buy, reduce sell.
      factor = 2**abs(self.config.log2_buy_sell_ratio)
      if sell_qty is not None:
        sell_qty /= factor
    elif self.config.log2_buy_sell_ratio < 0:
      # keep sell, reduce buy.
      factor = 2**abs(self.config.log2_buy_sell_ratio)
      if buy_qty is not None:
        buy_qty /= factor
    else:
      # no modification
      pass

    # adjust close_only
    if self.config2.close_only:
      pos = self._position - self.config2.reserve
      if pos > 0:
        sell_qty = min(sell_qty, abs(pos))
        buy_qty = 0
      elif pos < 0:
        buy_qty = min(buy_qty, abs(pos))
        sell_qty = 0
      else:
        sell_qty = 0
        buy_qty = 0

    # adjust min_pos, max_pos
    if self._position is not None:
      if sell_qty is not None:
        sell_qty = min(sell_qty, max(0, (self._position - self.config2.min_pos)))
      if buy_qty is not None:
        buy_qty = min(buy_qty, max(0, (self.config2.max_pos - self._position)))

    return sell_qty, buy_qty

  def _try_fire_sell_order(self, sell_price, sell_qty, amend_order=None):
    if not self._fire_sell:
      return

    with use_latency_recorder(self._latency_recorder,
                              LifeOfSignal.BEFORE_EXECUTOR_ORDER_SUBMIT,
                              LifeOfSignal.AFTER_EXECUTOR_ORDER_SUBMIT,
                              tag='try_fire_sell'):
      if amend_order is not None:
        assert amend_order.order_side == OrderSide.SELL, amend_order
        self._traits._amend_order(order_id=amend_order.order_id,
                                  product=self._product,
                                  price=sell_price,
                                  qty=sell_qty,
                                  order_side=OrderSide.SELL)
      else:
        self._traits._submit_pass_order(product=self._product,
                                        price=sell_price,
                                        qty=sell_qty,
                                        order_side=OrderSide.SELL)

  def _try_fire_buy_order(self, buy_price, buy_qty, amend_order=None):
    if not self._fire_buy:
      return

    with use_latency_recorder(self._latency_recorder,
                              LifeOfSignal.BEFORE_EXECUTOR_ORDER_SUBMIT,
                              LifeOfSignal.AFTER_EXECUTOR_ORDER_SUBMIT,
                              tag='try_buy_order'):
      if amend_order is not None:
        assert amend_order.order_side == OrderSide.BUY, amend_order
        self._traits._amend_order(order_id=amend_order.order_id,
                                  product=self._product,
                                  price=buy_price,
                                  qty=buy_qty,
                                  order_side=OrderSide.BUY)
      else:
        self._traits._submit_pass_order(product=self._product,
                                        price=buy_price,
                                        qty=buy_qty,
                                        order_side=OrderSide.BUY)

  def force_fire_sell_order(self, sell_price, sell_qty):
    with use_latency_recorder(self._latency_recorder,
                              LifeOfSignal.BEFORE_EXECUTOR_ORDER_SUBMIT,
                              LifeOfSignal.AFTER_EXECUTOR_ORDER_SUBMIT,
                              tag='force_fire_sell_order'):
      self._traits._submit_order(self._product, sell_price, sell_qty, OrderSide.SELL)

  def force_fire_buy_order(self, buy_price, buy_qty):
    with use_latency_recorder(self._latency_recorder,
                              LifeOfSignal.BEFORE_EXECUTOR_ORDER_SUBMIT,
                              LifeOfSignal.AFTER_EXECUTOR_ORDER_SUBMIT,
                              tag='force_fire_buy_order'):
      self._traits._submit_order(self._product, buy_price, buy_qty, OrderSide.BUY)

  def submit_mm_orders_impl(self,
                            *,
                            sell_price,
                            buy_price,
                            sell_qty,
                            buy_qty,
                            sell_price_pull,
                            buy_price_pull,
                            sell_price_push,
                            buy_price_push,
                            cancel_all_existing_sell,
                            cancel_all_existing_buy):
    if not self._og.is_ready():
      self._logger.info('OG is not ready, probably due to trade client restart!')
      return

    timestamp = get_timestamp()
    sell_amend_order = None
    buy_amend_order = None

    self._update_state(timestamp)
    if self.config.allow_amend:
      buy_amend_order = self._cancel_executor.extract_buy()
      sell_amend_order = self._cancel_executor.extract_sell()
    self._reset_action()

    sell_price, buy_price, sell_qty, buy_qty = self._adjust_request(sell_price, buy_price, sell_qty,
                                                                    buy_qty)

    self._submit_sell_order(timestamp=timestamp, sell_price=sell_price, sell_qty=sell_qty)
    self._submit_buy_order(timestamp=timestamp, buy_price=buy_price, buy_qty=buy_qty)

    if self._fire_sell or self._fire_buy:
      if self._lfiret.num_fires % 2 == 0:
        self._try_fire_sell_order(sell_price, sell_qty, sell_amend_order)
        self._try_fire_buy_order(buy_price, buy_qty, buy_amend_order)
      else:
        self._try_fire_buy_order(buy_price, buy_qty, buy_amend_order)
        self._try_fire_sell_order(sell_price, sell_qty, sell_amend_order)

    if sell_price_pull is not None or sell_price_push is not None:
      # TODO: this assert may fail due to price adjust.
      # assert sell_price > sell_price_pull, (sell_price, sell_price_pull)
      self._cancel_executor.cancel_sell_by_price(sell_price_pull, sell_price_push)
    if buy_price_pull is not None or buy_price_push is not None:
      # TODO: this assert may fail due to price adjust.
      # assert buy_price < buy_price_pull, (buy_price, buy_price_pull)
      self._cancel_executor.cancel_buy_by_price(buy_price_pull, buy_price_push)

    if cancel_all_existing_sell:
      self._cancel_executor.cancel_sell_working_orders()
    if cancel_all_existing_buy:
      self._cancel_executor.cancel_buy_working_orders()

    self._cancel_executor.remove_duplicates()
    if self._cancel_executor.has_cancel():
      self._traits._cancel_orders(self._cancel_executor.get_cancel_list())

    ret = False

    if self._fire_sell:
      self._lfiret.set_last_sell_fire(timestamp)
      ret = True

    if self._fire_buy:
      self._lfiret.set_last_buy_fire(timestamp)
      ret = True

    self._traits._check_state_impl()

    return ret

  def manage2_mm_orders_impl(self):
    if not self._og.is_ready():
      self._logger.info('OG is not ready, probably due to trade client restart!')
      return

    timestamp = get_timestamp()

    self._update_state(timestamp)
    self._reset_action()
    self._manage_risk(timestamp)

    self._cancel_executor.remove_duplicates()
    if self._cancel_executor.has_cancel():
      self._traits._cancel_orders(self._cancel_executor.get_cancel_list())

  def _manage_risk(self, timestamp):
    max_sell_qty, max_buy_qty = self._adjust_qty(
        self.config2.lot_size * self.config.max_working_cnt,
        self.config2.lot_size * self.config.max_working_cnt)
    self._cancel_executor.cancel_sell_with_risk(self._position, self.config2.min_pos, max_sell_qty)
    self._cancel_executor.cancel_buy_with_risk(self._position, self.config2.max_pos, max_buy_qty)
    self._cancel_executor.cancel_old_orders(timestamp - self.config.max_posting_period)

  def _submit_sell_order(self, *, timestamp, sell_price, sell_qty):
    ##########################################################################
    # check in the order of:
    # input check
    # pos range check
    # inflight order check
    # similarity check
    # throttle

    # TODO: add dry_run
    dry_run = False

    ##########################################################################
    # throttle

    if not self._sell_rate_limiter.check():
      return 10

    ##########################################################################
    # input check

    # if timestamp is wrong, do not fire.
    if timestamp is None:
      return 17

    # if price is None, just cancel all.
    if (sell_price is None):
      return 1

    # if qty is wrong, just cancel all.
    if (sell_qty is None or sell_qty <= 0):
      return 2

    # if qty is wrong,
    if (sell_qty > self.config2.lot_size):
      return 15

    ##########################################################################
    # pos range check

    # If current position is too big, do not fire.
    if (self._position < self.config2.min_pos):
      return 3

    # if qty is wrong, just cancel all.
    if (self._position - sell_qty < self.config2.min_pos):
      return 4

    # If potential position is too big, do not fire.
    if (self._wo._sell_potential + self._position - sell_qty < self.config2.min_pos):
      return 5

    # If sell potential is too big, do not fire.
    if (-1 * self._wo._sell_potential > self.config2.lot_size * self.config.max_working_cnt):
      return 6

    ##########################################################################
    # inflight order check

    # If number of inflight orders are too much, do not fire.
    if self._wo.get_sell_unack_count > self.config.max_inflight_cnt:
      return 7

    # If number of inflight orders are too much, do not fire.
    if self._wo.get_sell_cancel_unack_count > self.config.max_inflight_cnt:
      return 8

    ##########################################################################
    # throttle

    # If within order_update_period, do not fire.
    if self._lfiret._within_sell_update_period(timestamp):
      return 9

    # If within fill_cooldown_period, do not fire.
    if self._lfillt._within_sell_fill_cooldown_period(timestamp):
      return 14

    # If within pace control, do not fire.
    if self._sell_fill_limiter_qty and not self._sell_fill_limiter_qty.approve(timestamp, sell_qty):
      return 18

    ##########################################################################
    # similarity check

    # If there is an working order that its price is similar to
    # sell_price, keep it and do not fire.
    if self.config.HasField('sticky_price'):
      for order in self._wo._sell_orders:
        if _in_price(order.price, sell_price, self.config.sticky_price):
          return 11

    elif self.config.HasField('sticky_bp'):
      for order in self._wo._sell_orders:
        if _in_bp(order.price, sell_price, self.config.sticky_bp):
          return 12

    else:  # exact price match
      for order in self._wo._sell_orders:
        if order.price == sell_price:
          return 13

    ##########################################################################
    # fire

    if not dry_run:
      self._fire_sell = True
    return 0

  def _submit_buy_order(self, *, timestamp, buy_price, buy_qty):
    ##########################################################################
    # check in the order of:
    # input check
    # pos range check
    # inflight order check
    # similarity check
    # throttle

    # TODO: add dry_run
    dry_run = False

    ##########################################################################
    # throttle

    if not self._buy_rate_limiter.check():
      return 10

    ##########################################################################
    # input check

    # if timestamp is wrong, do not fire.
    if timestamp is None:
      return 17

    # if price is None, just cancel all.
    if (buy_price is None):
      return 1

    # if qty is wrong, just cancel all.
    if (buy_qty is None or buy_qty <= 0):
      return 2

    # if qty is wrong,
    if (buy_qty > self.config2.lot_size):
      return 15

    ##########################################################################
    # pos range check

    # If current position is too big, do not fire.
    if (self._position > self.config2.max_pos):
      return 3

    # If current position is too big, do not fire.
    if (self._position + buy_qty > self.config2.max_pos):
      return 4

    # If potential position is too big, do not fire.
    if (self._wo._buy_potential + self._position + buy_qty > self.config2.max_pos):
      return 5

    # If buy potential is too big, do not fire.
    if (self._wo._buy_potential > self.config2.lot_size * self.config.max_working_cnt):
      return 6

    ##########################################################################
    # inflight order check

    # If number of inflight orders are too much, do not fire.
    if self._wo.get_buy_unack_count > self.config.max_inflight_cnt:
      return 7

    # If number of inflight orders are too much, do not fire.
    if self._wo.get_buy_cancel_unack_count > self.config.max_inflight_cnt:
      return 8

    ##########################################################################
    # throttle

    # If within order_update_period, do not fire.
    if self._lfiret._within_buy_update_period(timestamp):
      return 9

    # If within fill_cooldown_period, do not fire.
    if self._lfillt._within_buy_fill_cooldown_period(timestamp):
      return 14

    # If within pace control, do not fire.
    if self._buy_fill_limiter_qty and not self._buy_fill_limiter_qty.approve(timestamp, buy_qty):
      return 18

    ##########################################################################
    # similarity check

    # If there is an working order that its price is similar to
    # buy_price, keep it and do not fire.
    if self.config.HasField('sticky_price'):
      for order in self._wo._buy_orders:
        if _in_price(order.price, buy_price, self.config.sticky_price):
          return 11

    elif self.config.HasField('sticky_bp'):
      for order in self._wo._buy_orders:
        if _in_bp(order.price, buy_price, self.config.sticky_bp):
          return 12

    else:  # exact price match
      for order in self._wo._buy_orders:
        if order.price == buy_price:
          return 13

    ##########################################################################
    # fire

    if not dry_run:
      self._fire_buy = True
    return 0
